package com.example.lock.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.util.ArrayList;
import java.util.ConcurrentModificationException;
import java.util.List;
import java.util.concurrent.TimeUnit;

@Service
public class DistributeLockService {

    public static final ThreadLocal<List<String>> LOCKS_HOLDER = new ThreadLocal<>();

    @Autowired
    private StringRedisTemplate redisTemplate;

    public void getLockForOneSeconds(String key) {
        getLock(key, 1, TimeUnit.SECONDS, 0, null);
    }

    public void getLock(String key, long lockTime, TimeUnit lockTimeUnit, long waitTime, TimeUnit waitTimeUnit) {
        long startTimestamp = System.currentTimeMillis();
        List<String> locks = LOCKS_HOLDER.get();
        if (locks != null && locks.contains(key)) {
            return;
        }
        try {
            boolean success = waitForSuccess(key, lockTime, lockTimeUnit, waitTime, waitTimeUnit, startTimestamp);
            if (!success) {
                throw new ConcurrentModificationException("Get distribute lock failure.");
            }
        } catch (InterruptedException e) {
            throw new RuntimeException("Get distribute lock failure.", e);
        }
        if (locks == null) {
            locks = new ArrayList<>();
            LOCKS_HOLDER.set(locks);
        }
        locks.add(key);
    }

    private boolean waitForSuccess(String key, long lockTime, TimeUnit lockTimeUnit, long waitTime, TimeUnit waitTimeUnit, long startTimestamp) throws InterruptedException {
        long endTimestamp = startTimestamp;
        if (waitTimeUnit != null) {
            endTimestamp = startTimestamp + waitTimeUnit.toMillis(waitTime);
        }
        boolean result = setIfAbsent(key, lockTime, lockTimeUnit);
        while (!result && System.currentTimeMillis() < endTimestamp) {
            TimeUnit.SECONDS.sleep(1);
            result = setIfAbsent(key, lockTime, lockTimeUnit);
        }
        return result;
    }

    private boolean setIfAbsent(String key, long lockTime, TimeUnit lockTimeUnit) {
        Boolean success = redisTemplate.opsForValue().setIfAbsent(key, "1", lockTime, lockTimeUnit);
        return success != null && success;
    }

    public void releaseLock(String key) {
        List<String> keys = DistributeLockService.LOCKS_HOLDER.get();
        if (!CollectionUtils.isEmpty(keys) && keys.contains(key)) {
            redisTemplate.delete(key);
            keys.remove(key);
        }
    }

    public void releaseAllLock() {
        List<String> keys = DistributeLockService.LOCKS_HOLDER.get();
        if (!CollectionUtils.isEmpty(keys)) {
            for (String key : keys) {
                redisTemplate.delete(key);
            }
            keys.clear();
        }
    }
}